#!/usr/bin/env python3
"""
fixstyle - A tool to fix formatting issues in git commits

This tool fixes common formatting issues in files modified in the latest git commit:
1. Converts tabs to spaces
2. Removes trailing spaces from lines
3. Removes tabs and spaces from empty lines
"""

import os
import sys
import subprocess
import argparse
from pathlib import Path
from typing import List, Set


class FixStyle:
    def __init__(self, tab_size: int = 4):
        self.tab_size = tab_size
        # Define binary file extensions that should be cleaned
        # Only include files that are compilation artifacts, not source files
        self.binary_extensions = {
            # Object files (compilation output)
            '.o', '.obj',
            # Static library files (compilation output)
            '.a', '.lib',
            # Shared library files (compilation output)
            '.so', '.dll', '.dylib',
            # Executable files (compilation output)
            '.exe', '.out',
            # Python compiled files (compilation output)
            '.pyc', '.pyo', '.pyd',
            # Java compiled files (compilation output)
            '.class',
            # Debug files (compilation output)
            '.pdb',
            # Temporary build files
            '.tmp', '.temp'
        }
    
    def get_modified_files(self) -> List[str]:
        """Get list of files modified in the latest commit"""
        try:
            # First try to get files modified in the latest commit
            result = subprocess.run(
                ['git', 'diff', '--name-only', 'HEAD~1', 'HEAD'],
                capture_output=True,
                text=True,
                check=True
            )
            files = result.stdout.strip().split('\n')
            # Handle cases where git shows paths with directory prefixes
            valid_files = []
            for f in files:
                if f:
                    # Try the file as-is first
                    if os.path.exists(f):
                        valid_files.append(f)
                    # If not found, try removing the first directory component
                    elif '/' in f:
                        basename = f.split('/', 1)[1]
                        if os.path.exists(basename):
                            valid_files.append(basename)
            return valid_files
        except subprocess.CalledProcessError:
            # If HEAD~1 doesn't exist (only one commit), get all files in the latest commit
            try:
                result = subprocess.run(
                    ['git', 'show', '--name-only', '--format=', 'HEAD'],
                    capture_output=True,
                    text=True,
                    check=True
                )
                files = result.stdout.strip().split('\n')
                # Handle cases where git shows paths with directory prefixes
                valid_files = []
                for f in files:
                    if f:
                        # Try the file as-is first
                        if os.path.exists(f):
                            valid_files.append(f)
                        # If not found, try removing the first directory component
                        elif '/' in f:
                            basename = f.split('/', 1)[1]
                            if os.path.exists(basename):
                                valid_files.append(basename)
                return valid_files
            except subprocess.CalledProcessError:
                print("Error: Unable to get git diff. Make sure you're in a git repository with commits.")
                return []
        except FileNotFoundError:
            print("Error: git command not found. Please install git.")
            return []
    
    def get_files_in_directory(self, directory: str) -> List[str]:
        """Get all source code files in the specified directory and its subdirectories"""
        source_files = []
        try:
            for root, dirs, files in os.walk(directory):
                for file in files:
                    file_path = os.path.join(root, file)
                    if self.is_source_code_file(file_path):
                        source_files.append(file_path)
            return source_files
        except OSError as e:
            print(f"Error: Unable to access directory {directory}: {e}")
            return []
    
    def find_files_by_name(self, file_pattern: str) -> List[str]:
        """Find files by name or path pattern in current directory and subdirectories"""
        found_files = []
        
        # If it's an exact path and exists, return it
        if os.path.exists(file_pattern):
            if self.is_source_code_file(file_pattern):
                return [file_pattern]
            else:
                print(f"Warning: {file_pattern} is not a source code file")
                return []
        
        # Search for files matching the pattern
        try:
            for root, dirs, files in os.walk('.'):
                for file in files:
                    file_path = os.path.join(root, file)
                    # Match by filename or relative path
                    if (file == file_pattern or 
                        file_path.replace('./', '') == file_pattern or
                        file_path.endswith('/' + file_pattern)):
                        if self.is_source_code_file(file_path):
                            found_files.append(file_path)
            return found_files
        except OSError as e:
            print(f"Error: Unable to search for files: {e}")
            return []
    
    def is_source_code_file(self, file_path: str) -> bool:
        """Check if file is a source code file"""
        # Define supported source code file extensions
        source_extensions = {
            '.py', '.pyx', '.pyi',  # Python
            '.java', '.kt', '.kts',  # Java, Kotlin
            '.c', '.h', '.cpp', '.cxx', '.cc', '.hpp', '.hxx', '.hh',  # C/C++
            '.cs',  # C#
            '.js', '.jsx', '.ts', '.tsx',  # JavaScript, TypeScript
            '.php',  # PHP
            '.rb',  # Ruby
            '.go',  # Go
            '.rs',  # Rust
            '.swift',  # Swift
            '.m', '.mm',  # Objective-C
            '.scala',  # Scala
            '.pl', '.pm',  # Perl
            '.sh', '.bash', '.zsh',  # Shell scripts
            '.sql',  # SQL
            '.r', '.R',  # R
            '.lua',  # Lua
            '.dart',  # Dart
            '.groovy',  # Groovy
            '.vb',  # Visual Basic
            '.f', '.f90', '.f95', '.f03', '.f08',  # Fortran
            '.pas',  # Pascal
            '.asm', '.s',  # Assembly
            '.ml', '.mli',  # OCaml
            '.hs',  # Haskell
            '.elm',  # Elm
            '.clj', '.cljs',  # Clojure
            '.ex', '.exs',  # Elixir
            '.erl', '.hrl',  # Erlang
            '.bp',  # Android.bp files
        }
        
        file_ext = Path(file_path).suffix.lower()
        return file_ext in source_extensions
    
    def is_binary_artifact(self, file_path: str) -> bool:
        """Check if file is a binary artifact that should be cleaned"""
        file_ext = Path(file_path).suffix.lower()
        return file_ext in self.binary_extensions
    
    def is_text_file(self, file_path: str) -> bool:
        """Check if file is a text file (fallback check)"""
        try:
            with open(file_path, 'rb') as f:
                chunk = f.read(1024)
                return b'\0' not in chunk
        except (IOError, OSError):
            return False
    
    def fix_file_formatting(self, file_path: str) -> bool:
        """Fix formatting issues in a single file"""
        # First check if it's a source code file
        if not self.is_source_code_file(file_path):
            return False
        
        # Then check if it's a text file (fallback)
        if not self.is_text_file(file_path):
            return False
        
        try:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()
        except (IOError, OSError, UnicodeDecodeError):
            print(f"Warning: Could not read file {file_path}")
            return False
        
        modified = False
        fixed_lines = []
        
        for line in lines:
            original_line = line
            
            # 1. Convert tabs to spaces
            if '\t' in line:
                line = line.expandtabs(self.tab_size)
                modified = True
            
            # 2. Remove trailing spaces from non-empty lines
            if line.strip() != '':
                stripped_line = line.rstrip()
                if line.endswith('\n') and not stripped_line.endswith('\n'):
                    stripped_line += '\n'
                if line != stripped_line:
                    line = stripped_line
                    modified = True
            
            # 3. Remove tabs and spaces from empty lines
            if line.strip() == '':
                line = '\n' if line.endswith('\n') else ''
                if original_line != line:
                    modified = True
            
            fixed_lines.append(line)
        
        if modified:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.writelines(fixed_lines)
                return True
            except (IOError, OSError):
                print(f"Error: Could not write to file {file_path}")
                return False
        
        return False
    
    def clean_binary_artifacts(self) -> None:
        """Remove binary artifact files from the latest commit"""
        modified_files = self.get_modified_files()
        if not modified_files:
            print("No modified files found in the latest commit.")
            return
        
        print(f"Found {len(modified_files)} modified files in the latest commit.")
        
        deleted_count = 0
        skipped_count = 0
        
        for file_path in modified_files:
            if self.is_binary_artifact(file_path):
                try:
                    if os.path.exists(file_path):
                        os.remove(file_path)
                        print(f"  ✓ Deleted binary artifact: {file_path}")
                        deleted_count += 1
                    else:
                        print(f"  - File not found (already deleted): {file_path}")
                        skipped_count += 1
                except OSError as e:
                    print(f"  ✗ Failed to delete {file_path}: {e}")
                    skipped_count += 1
            else:
                print(f"  - Skipped (not a binary artifact): {file_path}")
                skipped_count += 1
        
        print(f"\nSummary: Deleted {deleted_count} binary artifacts.")
        if skipped_count > 0:
            print(f"Skipped {skipped_count} non-binary files.")
        
        if deleted_count > 0:
            print("\nNote: Binary artifacts have been deleted. You may want to commit these changes.")
    
    def fix_all_modified_files(self) -> None:
        """Fix formatting issues in all modified files"""
        modified_files = self.get_modified_files()
        
        if not modified_files:
            print("No modified files found in the latest commit.")
            return
        
        print(f"Found {len(modified_files)} modified files in the latest commit.")
        
        fixed_count = 0
        skipped_count = 0
        for file_path in modified_files:
            print(f"Processing: {file_path}")
            
            # Check if it's a source code file first
            if not self.is_source_code_file(file_path):
                print(f"  - Skipped (not a source code file)")
                skipped_count += 1
                continue
            
            if self.fix_file_formatting(file_path):
                print(f"  ✓ Fixed formatting issues")
                fixed_count += 1
            else:
                print(f"  - No changes needed")
        
        processed_count = len(modified_files) - skipped_count
        print(f"\nSummary: Fixed formatting in {fixed_count} out of {processed_count} source code files.")
        if skipped_count > 0:
            print(f"Skipped {skipped_count} non-source files.")
        
        if fixed_count > 0:
            print("\nNote: Files have been modified. You may want to commit these changes.")


def main():
    parser = argparse.ArgumentParser(
        description='Fix formatting issues in files modified in the latest git commit'
    )
    parser.add_argument(
        '--tab-size',
        type=int,
        default=4,
        help='Number of spaces to replace each tab with (default: 4)'
    )
    parser.add_argument(
        '--clean-binaries',
        action='store_true',
        help='Delete binary artifact files (*.o, *.so, *.a, *.pyc, etc.) from the latest commit'
    )
    parser.add_argument(
        '-d', '--directory',
        type=str,
        help='Process all source files in specified directory and subdirectories'
    )
    parser.add_argument(
        '-f', '--file',
        type=str,
        help='Process specific file by name or relative path'
    )
    parser.add_argument(
        '--version',
        action='version',
        version='fixstyle 1.0.1'
    )
    
    args = parser.parse_args()
    
    # Check if we're in a git repository (only for default behavior)
    if not args.directory and not args.file:
        if not os.path.exists('.git') and not subprocess.run(['git', 'rev-parse', '--git-dir'], capture_output=True).returncode == 0:
            print("Error: Not in a git repository.")
            sys.exit(1)
    
    fixer = FixStyle(tab_size=args.tab_size)
    
    if args.clean_binaries:
        fixer.clean_binary_artifacts()
    elif args.directory:
        # Process files in specified directory
        if not os.path.exists(args.directory):
            print(f"Error: Directory '{args.directory}' does not exist.")
            sys.exit(1)
        if not os.path.isdir(args.directory):
            print(f"Error: '{args.directory}' is not a directory.")
            sys.exit(1)
        
        source_files = fixer.get_files_in_directory(args.directory)
        if not source_files:
            print(f"No source code files found in directory '{args.directory}'.")
            return
        
        print(f"Found {len(source_files)} source file(s) in directory '{args.directory}':")
        for file in source_files:
            print(f"  - {file}")
        
        fixed_count = 0
        for file_path in source_files:
            if fixer.fix_file_formatting(file_path):
                fixed_count += 1
        
        print(f"\nSummary: Fixed formatting in {fixed_count} source file(s).")
    elif args.file:
        # Process specific file
        source_files = fixer.find_files_by_name(args.file)
        if not source_files:
            print(f"No source code files found matching '{args.file}'.")
            return
        
        print(f"Found {len(source_files)} source file(s) matching '{args.file}':")
        for file in source_files:
            print(f"  - {file}")
        
        fixed_count = 0
        for file_path in source_files:
            if fixer.fix_file_formatting(file_path):
                fixed_count += 1
        
        print(f"\nSummary: Fixed formatting in {fixed_count} source file(s).")
    else:
        fixer.fix_all_modified_files()


if __name__ == '__main__':
    main()